package tk.mybatis.util;

import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Modifier;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Field 辅助类
 */
@SuppressWarnings({ "unchecked" })
public class Fields {
	// name --> Field
	private static final Map<String, Field> nameToFieldCache = new ConcurrentHashMap<String, Field>(16);

	// ---------------------------------------------------------------------
	// Static API used as entrance points to the fluent API
	// ---------------------------------------------------------------------

	public static <T> T get(Object object, String name) {
		return get(object, name, object.getClass());
	}

	public static <T> T get(Object object, String name, Class<?> type) {
		return new Fields(type).getValue(object, name);
	}

	public static void set(Object object, String name, Object value) {
		set(object, name, value, object.getClass());
	}

	public static void set(Object object, String name, Object value, Class<?> type) {
		new Fields(type).setValue(object, name, value);
	}

	public static <T extends AccessibleObject> T accessible(T accessible) {
		if (accessible == null) {
			return null;
		}

		if (accessible instanceof Member) {
			Member member = (Member) accessible;

			if (Modifier.isPublic(member.getModifiers()) && Modifier.isPublic(member.getDeclaringClass().getModifiers())) {

				return accessible;
			}
		}

		if (!accessible.isAccessible()) {
			accessible.setAccessible(true);
		}

		return accessible;
	}

	// ---------------------------------------------------------------------
	// Members
	// ---------------------------------------------------------------------
	private final Class<?> type;

	// ---------------------------------------------------------------------
	// Constructors
	// ---------------------------------------------------------------------

	public Fields(String name, ClassLoader classLoader) {
		this(forName(name, classLoader));
	}

	public Fields(String name) {
		this(forName(name));
	}

	public Fields(Class<?> type) {
		this.type = type;
	}

	// ---------------------------------------------------------------------
	// Fields API
	// ---------------------------------------------------------------------
	public void setValue(Object object, String name, Object value) {
		setValue0(object, type, name, value);
	}

	public <T> T getValue(Object object, String name) {
		return getValue0(object, type, name);
	}

	/**
	 * @see type.isAssignableFrom(field.getDeclaringClass())
	 */
	public boolean isAssignableFrom(Field field) {
		return type.isAssignableFrom(field.getDeclaringClass());
	}

	// ---------------------------------------------------------------------
	// Utility methods
	// ---------------------------------------------------------------------
	private static void setValue0(Object object, Class<?> type, String name, Object value) {
		FieldTokenizer token = new FieldTokenizer(name);
		if (token.hasChildren()) {
			object = getValue0(object, type, token.getName());
			type = object.getClass();
			name = token.getChildren();
			setValue0(object, type, name, value);
		} else {
			try {
				field0(type, name).set(object, value);
			} catch (Exception e) {
				throw new FieldsException(e);
			}
		}
	}

	private static <T> T getValue0(Object object, Class<?> type, String name) {
		FieldTokenizer token = new FieldTokenizer(name);
		if (token.hasChildren()) {
			object = getValue0(object, type, token.getName());
			type = object.getClass();
			name = token.getChildren();
			return getValue0(object, type, name);
		} else {
			try {
				return (T) field0(type, name).get(object);
			} catch (Exception e) {
				throw new FieldsException(e);
			}
		}
	}

	private static Field getField(String name) {
		return nameToFieldCache.get(name);
	}

	private static Field putField(String name, Field field) {
		nameToFieldCache.put(name, field);
		return field;
	}

	private static Field field0(Class<?> type, String name) {
		String key = type.getName() + "." + name;
		Field field = getField(key);

		if (field != null) return field;

		// Try getting a public field
		try {
			field = type.getField(name);
			return putField(key, field);
		}
		// Try again, getting a non-public field
		catch (NoSuchFieldException e) {
			do {
				try {
					field = accessible(type.getDeclaredField(name));
					return putField(key, field);
				} catch (NoSuchFieldException ignore) {}

				type = type.getSuperclass();
				key = type.getName() + "." + name;
				field = getField(key);
				if (field != null) return field;

			} while (type != null);

			throw new FieldsException(e);
		}
	}

	/**
	 * Load a class
	 *
	 * @see Class#forName(String)
	 */
	private static Class<?> forName(String name) {
		try {
			return Class.forName(name);
		} catch (Exception e) {
			throw new FieldsException(e);
		}
	}

	private static Class<?> forName(String name, ClassLoader classLoader) {
		try {
			return Class.forName(name, true, classLoader);
		} catch (Exception e) {
			throw new FieldsException(e);
		}
	}

	private static class FieldTokenizer {
		private String name;
		private String children;

		public FieldTokenizer(String fullname) {
			int delim = fullname.indexOf('.');
			if (delim > -1) {
				name = fullname.substring(0, delim);
				children = fullname.substring(delim + 1);
			} else {
				name = fullname;
				children = null;
			}
		}

		public String getName() {
			return name;
		}

		public String getChildren() {
			return children;
		}

		public boolean hasChildren() {
			return children != null;
		}
	}
}
